package org.moonshot.chatty.parse;

import java.util.HashSet;

import org.moonshot.chatty.ChattyInternalError;
import org.moonshot.chatty.ChattyParseException;
import org.moonshot.chatty.lex.ChattyLexeme;
import org.moonshot.chatty.lex.ChattyLexer;
import org.moonshot.chatty.lex.LexemeType;

public class ChattyParser
{
	/**
	 * @param aMethod
	 * @return
	 * @throws ChattyParseException
	 */
	public ChattyAST parse (CharSequence aMethod)
		throws ChattyParseException, ChattyInternalError
	{
		ChattyAST root = null;
		try
		{
			ChattyLexer L = new ChattyLexer (" " + aMethod + " ");
			
			root = parseMethod (L);
		}
		catch( ChattyParseException e )
		{
			throw e;
		}
		catch( ChattyInternalError e )
		{
			throw e;
		}
		catch( Exception e )
		{
			throw new ChattyInternalError (e, "unknown failure");
		}
		
		return root;
	}
	
	/**
	 * Parse a complete method, from signature to terminal statement.
	 * 
	 * @param aL
	 * @return
	 * @throws ChattyInternalError
	 * @throws ChattyParseException
	 */
	private ChattyAST parseMethod (ChattyLexer aL) throws ChattyInternalError, ChattyParseException
	{
		final ChattyAST root = new ChattyAST (ASTType.METHOD);
		
		// Initialize the common elements
		final ChattyAST body = new ChattyAST (ASTType.SEQUENCE);
		root.put (ASTKey.BODY, body);
		
		final ChattyAST vars = new ChattyAST (ASTType.SEQUENCE);
		root.put (ASTKey.METHOD_VARS, vars);
		
		final ChattyAST args = new ChattyAST (ASTType.SEQUENCE);
		root.put (ASTKey.METHOD_ARGS, args);
		
		// Parse the signature.
		ChattyLexeme le = expect (aL, LexemeType.IDENT, LexemeType.BINOP, LexemeType.FRAGMENT);
		
		// It starts with an optional scope; INSTANCE by default.
		MethodScope scope = MethodScope.INSTANCE;
		if( le.type == LexemeType.IDENT )
		{
			if( le.text.equals ("class") )
			{
				scope = MethodScope.CLASS;
				le = expect (aL, LexemeType.IDENT, LexemeType.BINOP, LexemeType.FRAGMENT);
			}
			else if( le.text.equals ("new") )
			{
				scope = MethodScope.CONSTRUCTOR;
				le = expect (aL, LexemeType.IDENT, LexemeType.BINOP, LexemeType.FRAGMENT);
			}
		}
		root.put (ASTKey.METHOD_SCOPE, scope);
		
		// It continues with a signature.
		if( le.type == LexemeType.IDENT )
		{
			// Unary method signature
			root.put (ASTKey.METHOD_NAME, le.text);
			root.put (ASTKey.METHOD_TYPE, MethodType.UNARY);
			
			le = aL.next ();
		}
		else if( le.type == LexemeType.BINOP )
		{
			// Binary method signature
			root.put (ASTKey.METHOD_NAME, le.text);
			root.put (ASTKey.METHOD_TYPE, MethodType.BINARY);
			
			le = expect (aL, LexemeType.IDENT);
			args.push (le.text);
			
			le = aL.next ();
		}
		else if( le.type == LexemeType.FRAGMENT )
		{
			// Keyword method signature
			root.put (ASTKey.METHOD_TYPE, MethodType.KEYWORD);
			String theFullName = le.text;
			
			le = expect (aL, LexemeType.IDENT);
			args.push (le.text);
			
			le = aL.next ();
			while( le.type == LexemeType.FRAGMENT )
			{
				theFullName = theFullName + le.text;
				
				le = expect (aL, LexemeType.IDENT);
				args.push (le.text);
				
				le = aL.next ();
			}
			
			root.put (ASTKey.METHOD_NAME, theFullName);
		}
		
		if( le.type == LexemeType.BAR )
		{
			// Parse the locals declaration before handing off to the generic
			// body parser.
			le = expect (aL, LexemeType.BAR, LexemeType.IDENT);
			while( le.type != LexemeType.BAR )
			{
				vars.push (le.text);
				le = expect (aL, LexemeType.BAR, LexemeType.IDENT);
			}
		}
		else
		{
			aL.back (le);
		}
		
		parseBody (aL, body);
		
		if( aL.peek ().type != LexemeType.EOF )
			throw new ChattyParseException (0, 0, aL.peek (),
			        "Syntax error: end of method, but document continues");
		
		return root;
	}
	
	/**
	 * Parse a series of statements, into the given body sequence. Terminates at
	 * EOF or closing bracket (eating it if encountered). Returns true if
	 * terminated with EOF, false otherwise.
	 * 
	 * @param aL
	 *            The lexer to stream tokens from.
	 * @param aBody
	 *            The existing wrapper for the body sequence.
	 * @throws ChattyInternalError
	 * @throws ChattyParseException
	 */
	private void parseBody (ChattyLexer aL, ChattyAST aBody) throws ChattyInternalError,
	        ChattyParseException
	{
		ChattyAST theNode = parseExpr (aL, true, LexemeType.PERIOD, LexemeType.EOF);
		
		if( aL.peek ().type == LexemeType.EOF )
		{
			if( theNode.type != ASTType.RETURN )
			{
				ChattyAST theWrap = new ChattyAST (ASTType.RETURN);
				theWrap.put (ASTKey.VALUE, theNode);
				theNode = theWrap;
			}
			aBody.push (theNode);
			return;
		}
		
		// Eat the leftover period.
		if( aL.peek ().type == LexemeType.PERIOD )
			aL.next ();
		
		aBody.push (theNode);
		
		while( true )
		{
			theNode = parseExpr (aL, true, LexemeType.PERIOD, LexemeType.EOF);
			aBody.push (theNode);
			
			ChattyLexeme le = aL.next ();
			
			if( le.type == LexemeType.EOF )
				break;
			
			if( le.type != LexemeType.PERIOD )
				throw new ChattyParseException (le.start, le.end, le, "Syntax error: unexpected "
				        + le.type.readable);
			
			if( aL.peek ().type == LexemeType.EOF )
				break;
		}
	}
	
	/**
	 * Parse an expression, expecting it to end with ) or ]
	 * 
	 * @param aL
	 * @throws ChattyInternalError
	 * @throws ChattyParseException
	 */
	private ChattyAST parseExpr (ChattyLexer aL, boolean allowReturn, LexemeType... aExpected)
	        throws ChattyInternalError, ChattyParseException
	{
		ChattyLexeme le = aL.next ();
		ChattyAST theNode;
		ChattyAST theHead;
		
		switch( le.type )
		{
		case RETURN:
			if( !allowReturn )
				throw new ChattyParseException (le.start, le.end, le, "Syntax error: unexpected return");
			
			theNode = new ChattyAST (ASTType.RETURN);
			theNode.put (ASTKey.VALUE, parseExpr (aL, false, aExpected));
			return theNode;
			
		case LITARR:
			theHead = parseLitArr(aL);
			
			return parseTailExpr (theHead, aL, aExpected);
			
		case OPAREN:
			theHead = parseExprNonAssign (aL, LexemeType.CPAREN);
			le = expect (aL, LexemeType.CPAREN);
			le = aL.peek ();
			if( le.type.matchAny (aExpected) )
				return theHead;
			
			return parseTailExpr (theHead, aL, aExpected);
			
		case OBRACK:
			theHead = parseBlock (aL, le);
			le = aL.peek ();
			if( le.type.matchAny (aExpected) )
			{
				aL.next ();
				return theHead;
			}
			return parseTailExpr (theHead, aL, aExpected);
			
		case IDENT:
			ChattyLexeme l2 = aL.peek ();
			if( l2.type != LexemeType.ASSIGN )
			{
				aL.back (le);
				return parseExprNonAssign (aL, aExpected);
			}
			
			aL.next ();
			theNode = new ChattyAST (ASTType.ASSIGN);
			theNode.put (ASTKey.ASSIGN_TO, le.text);
			theNode.put (ASTKey.VALUE, parseExpr (aL, false, aExpected));
			return theNode;
			
		case STRING:
		case NUMBER:
			aL.back (le);
			return parseExprNonAssign (aL, aExpected);
		}
		
		throw new ChattyParseException (le.start, le.end, le, "Syntax error: " + le.type.readable
		        + " unexpected");
	}
	
	private ChattyAST parseLitArr (ChattyLexer aL)
    	throws ChattyInternalError, ChattyParseException
    {
		final ChattyAST theArray = new ChattyAST (ASTType.ARRAY_LITERAL);
		ChattyLexeme le = aL.next ();
		
		try {
			ChattyAST theNode;
			
			while( le.type != LexemeType.CPAREN )
    		{
    			switch( le.type )
    			{
    			case NUMBER:
    				theNode = new ChattyAST (ASTType.EVAL);
    				theNode.put (ASTKey.EVAL_TEXT, le.text);
    				theNode.put (ASTKey.EVAL_KIND, EvalType.NUMBER);
    				theArray.push (theNode);
    				break;
    			
    			case STRING:
    				theNode = new ChattyAST (ASTType.EVAL);
    				theNode.put (ASTKey.EVAL_TEXT, le.text);
    				theNode.put (ASTKey.EVAL_KIND, EvalType.STRING);
    				theArray.push (theNode);
    				break;
    			
    			case IDENT:
    				theNode = new ChattyAST (ASTType.EVAL);
    				theNode.put (ASTKey.EVAL_TEXT, le.text);
    				theNode.put (ASTKey.EVAL_KIND, EvalType.IDENTIFIER);
    				theArray.push (theNode);
    				break;
    			
    			case OBRACK:
    				theNode = parseBlock (aL, le);
    				theArray.push (theNode);
    				break;
    			
    			case LITARR:
    				theNode = parseLitArr(aL);
    				theArray.push (theNode);
    				break;
    				
    			case OPAREN:
    				theNode = parseExprNonAssign (aL, LexemeType.CPAREN);
    				expect (aL, LexemeType.CPAREN);
    				theArray.push (theNode);
    				break;
    			}
    			
    			le = aL.next ();
    		}
		}
		catch ( NullPointerException e ) {
			throw new ChattyParseException (le.start, le.end, null, "Unexpected end of file");
		}
		
	    return theArray;
    }

	private LexemeType[] and (LexemeType[] aBase, LexemeType... aMore)
	{
		HashSet<LexemeType> aSet = new HashSet<LexemeType> ();
		for( int i = 0; i < aBase.length; i++ )
			aSet.add (aBase[i]);
		for( int i = 0; i < aMore.length; i++ )
			aSet.add (aMore[i]);
		
		LexemeType[] aRet = new LexemeType[aSet.size ()];
		return aSet.toArray (aRet);
	}
	
	private ChattyAST parseTailExpr (ChattyAST aHead, ChattyLexer aL, LexemeType[] aExpected)
	        throws ChattyInternalError, ChattyParseException
	{
		LexemeType[] expectedInternal = and (aExpected, LexemeType.FRAGMENT, LexemeType.BINOP);
		ChattyLexeme le = aL.next ();
		ChattyAST theNode = null;
		
		if( le.type.matchAny (aExpected) )
		{
			aL.back (le);
			return aHead;
		}
		
		switch( le.type )
		{
		case COMMA:
			return parseTailExpr (aHead, aL, aExpected);
		
		case IDENT:
			theNode = new ChattyAST (ASTType.CALL_UNARY);
			theNode.put (ASTKey.VALUE, aHead);
			theNode.put (ASTKey.UNARY_NAME, le.text);
			return parseTailExpr (theNode, aL, aExpected);
			
		case BINOP:
			theNode = new ChattyAST (ASTType.CALL_BINARY);
			theNode.put (ASTKey.VALUE, aHead);
			theNode.put (ASTKey.BINARY_NAME, le.text);
			theNode.put (ASTKey.BINARY_VALUE, parseExprNonAssign (aL, expectedInternal));
			return parseTailExpr (theNode, aL, aExpected);
			
		case FRAGMENT:
			aL.back (le);
			theNode = parseKeywordMessage (aHead, aL, aExpected);
			return parseTailExpr (theNode, aL, aExpected);
		}
		
		throw new ChattyParseException (le.start, le.end, le, "Expected message: "
		        + name (LexemeType.IDENT, LexemeType.BINOP, LexemeType.FRAGMENT));
	}
	
	private ChattyAST parseKeywordMessage (ChattyAST aHead, ChattyLexer aL, LexemeType[] aExpected)
	        throws ChattyInternalError, ChattyParseException
	{
		LexemeType[] expectedInline = and (aExpected, LexemeType.COMMA, LexemeType.FRAGMENT);
		
		ChattyLexeme le = aL.next ();
		
		ChattyAST theNode = new ChattyAST (ASTType.CALL_KEYWORD);
		theNode.put (ASTKey.VALUE, aHead);
		
		ChattyAST args = new ChattyAST (ASTType.SEQUENCE);
		theNode.put (ASTKey.KEYWORD_VALUES, args);
		
		StringBuilder theName = new StringBuilder ();
		while( !le.type.matchAny (aExpected) && le.type == LexemeType.FRAGMENT )
		{
			theName.append (le.text);
			args.push (parseExprNonAssign (aL, expectedInline));
			le = aL.next ();
		}
		theNode.put (ASTKey.KEYWORD_NAME, theName.toString ());
		
		if( !le.type.matchAny (expectedInline) )
			throw new ChattyParseException (le.start, le.end, le, "Syntax error: expected "
			        + name (and (aExpected, LexemeType.FRAGMENT)));
		
		aL.back (le);
		return theNode;
	}
	
	private ChattyAST parseExprNonAssign (ChattyLexer aL, LexemeType... aExpected)
	        throws ChattyParseException, ChattyInternalError
	{
		ChattyLexeme le = aL.next ();
		ChattyAST theNode = null;
		
		switch( le.type )
		{
		case NUMBER:
			theNode = new ChattyAST (ASTType.EVAL);
			theNode.put (ASTKey.EVAL_TEXT, le.text);
			theNode.put (ASTKey.EVAL_KIND, EvalType.NUMBER);
			break;
		
		case STRING:
			theNode = new ChattyAST (ASTType.EVAL);
			theNode.put (ASTKey.EVAL_TEXT, le.text);
			theNode.put (ASTKey.EVAL_KIND, EvalType.STRING);
			break;
		
		case IDENT:
			theNode = new ChattyAST (ASTType.EVAL);
			theNode.put (ASTKey.EVAL_TEXT, le.text);
			theNode.put (ASTKey.EVAL_KIND, EvalType.IDENTIFIER);
			break;
		
		case OBRACK:
			theNode = parseBlock (aL, le);
			break;
		
		case LITARR:
			theNode = parseLitArr(aL);
			break;
			
		case OPAREN:
			theNode = parseExprNonAssign (aL, LexemeType.CPAREN);
			expect (aL, LexemeType.CPAREN);
			break;
		}
		
		if( theNode == null )
			throw new ChattyParseException (le.start, le.end, le, "Syntax error: " + le.type.readable
			        + " unexpected");
		
		if( aL.peek ().type.matchAny (aExpected) )
			return theNode;
		
		return parseTailExpr (theNode, aL, aExpected);
	}
	
	private ChattyAST parseBlock (ChattyLexer aL, ChattyLexeme aLe) throws ChattyInternalError, ChattyParseException
	{
		final ChattyAST block = new ChattyAST (ASTType.BLOCK);
		
		final ChattyAST args = new ChattyAST (ASTType.SEQUENCE);
		block.put (ASTKey.BLOCK_ARGS, args);
		
		final ChattyAST body = new ChattyAST (ASTType.SEQUENCE);
		block.put (ASTKey.BODY, body);
		
		block.put (ASTKey.AT_START, aLe.start);
		
		ChattyLexeme le = aL.peek ();
		if( le.type == LexemeType.FRAGMENT )
		{
			aL.next ();
			args.push (le.text.substring (0, le.text.length () - 1));
			le = expect (aL, LexemeType.BAR, LexemeType.FRAGMENT);
			while( le.type != LexemeType.BAR )
			{
				args.push (le.text.substring (0, le.text.length () - 1));
				le = expect (aL, LexemeType.BAR, LexemeType.FRAGMENT);
			}
		}
		
		ChattyAST theNode = parseExpr (aL, true, LexemeType.PERIOD, LexemeType.CBRACK);
		while( true )
		{
			le = aL.peek ();
			int thePos = le.start;
			
			body.push (theNode);
			
			if( theNode.type == ASTType.RETURN )
				break;
			
			if( le.type == LexemeType.CBRACK )
				break;
			
			le = expect (aL, LexemeType.PERIOD);
			if( aL.peek ().type == LexemeType.CBRACK )
				break;
			
			theNode = parseExpr (aL, true, LexemeType.PERIOD, LexemeType.CBRACK);
			
			if( theNode == null )
				throw new ChattyParseException (thePos, aL.peek ().end, aL.peek (),
				        "Syntax error: expected expression");
		}
		
		le = expect (aL, LexemeType.CBRACK);
		block.put(ASTKey.AT_END, le.end);
		
		return block;
	}
	
	/**
	 * Read a single lexeme, and expect it to be of a specific type.
	 * 
	 * @param aL
	 * @param aType
	 * @return
	 * @throws ChattyParseException
	 */
	private ChattyLexeme expect (ChattyLexer aL, LexemeType aType) throws ChattyParseException
	{
		ChattyLexeme retval = aL.next ();
		
		if( retval == null && aType != null )
			throw new ChattyParseException (0, 0, null, "Syntax error: unexpected end of document");
		
		if( retval != null && retval.type != aType )
			throw new ChattyParseException (retval.start, retval.end, retval,
					"Syntax error: " + aType.readable + " expected");
		
		return retval;
	}
	
	/**
	 * Read a single lexeme, and expect it to be one of a set of types.
	 * 
	 * @param aL
	 * @param aTypes
	 * @return
	 * @throws ChattyParseException
	 */
	private ChattyLexeme expect (ChattyLexer aL, LexemeType... aTypes) throws ChattyParseException
	{
		ChattyLexeme retval = aL.next ();
		boolean found = false;
		for( LexemeType theType : aTypes )
			if( (retval == null && theType == null) || retval.type == theType )
			{
				found = true;
				break;
			}
		
		if( !found )
		{
			if( retval == null )
				throw new ChattyParseException (0, 0, null, "Syntax error: unexpected end of document");
			
			throw new ChattyParseException (retval.start, retval.end, retval,
					"Syntax error: " + retval.type.readable + " unexpected");
		}
		
		return retval;
	}
	
	/**
	 * Give a readable name for what was expected
	 */
	private String name (LexemeType... aLexemes)
	{
		StringBuilder aBuild = new StringBuilder ();
		aBuild.append (aLexemes[0].readable);
		for( int i = 1; i < aLexemes.length; i++ )
		{
			aBuild.append (", ");
			aBuild.append (aLexemes[i].readable);
		}
		return aBuild.toString ();
	}
}
